package com.sdust.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.IndexResponse;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.CompletionSuggestOption;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sdust.constant.MessageConstant;
import com.sdust.constant.ParameterConstant;
import com.sdust.context.BaseContext;
import com.sdust.mapper.ArticleMapper;
import com.sdust.pojos.dto.*;
import com.sdust.pojos.entity.Article;
import com.sdust.pojos.entity.ArticleDoc;
import com.sdust.pojos.entity.Comment;
import com.sdust.pojos.enums.AppHttpCodeEnum;
import com.sdust.result.Result;
import com.sdust.service.ArticleService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.message.BasicHeader;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;


@Service
@RequiredArgsConstructor
@Slf4j
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {

    private final ArticleMapper articleMapper;

    private final MongoTemplate mongoTemplate;

    private final RedisTemplate redisTemplate;

    /**
     * 根据id查询文章
     * @param id
     * @return
     */
    @Override
    public Result selectById(Integer id) {

        Article article = articleMapper.selectById(id);
        if(article == null) {
            return Result.error("文章不存在");
        }

        return Result.success(article);
    }

    /**
     * 新增文章
     * @param dto
     * @return
     */
    @Override
    public Result addNewArticle(ArticleDto dto) {

        //参数判断
        if(dto == null) {
            return Result.error(AppHttpCodeEnum.PARAM_INVALID);
        }
        if(StringUtils.isBlank(dto.getAuthorName()) || StringUtils.isBlank(dto.getContent()) || StringUtils.isBlank(dto.getTitle()) || StringUtils.isBlank(dto.getInfo())) {
            return Result.error(AppHttpCodeEnum.PARAM_INVALID);
        }

        //构建返回对象
        Article article = new Article();
        BeanUtils.copyProperties(dto, article);

        //设置当前上传用户id
        Integer id = BaseContext.getCurrentId();
        if(id == null) {
            return Result.error(MessageConstant.USER_NOT_LOGIN);
        }
        article.setAuthorId(id);

        article.setUpdateTime(LocalDateTime.now());

        Integer lastInsertId = articleMapper.addNewArticle(article);

        //获取刚插入数据表的文章id
        log.info("刚插入数据库的文章id：{}", article.getId());

        //构建成Doc文档插入索引库
        ArticleDoc doc = new ArticleDoc(article);

        //文章数据插入ES
        // URL and API key
        String serverUrl = "http://localhost:9200";
        String apiKey = "hz0gC8IiQiC588ePvr_DOQ";

        // Create the low-level client
        RestClient restClient = RestClient
                .builder(HttpHost.create(serverUrl))
                .setDefaultHeaders(new Header[]{
                        new BasicHeader("Authorization", "ApiKey " + apiKey)
                })
                .build();

        // Create the transport with a Jackson mapper
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());

        // And create the API client
        ElasticsearchClient esClient = new ElasticsearchClient(transport);


        // Use the client...
        IndexResponse response = null;
        try {
            response = esClient.index(i -> i
                    .index("ap_article")
                    .id(doc.getId().toString())
                    .document(doc)
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // Close the client, also closing the underlying transport object and network connections.
        try {
            esClient.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return Result.success("新增文章成功");
    }

    /**
     * 查询文章列表
     * @param dto
     * @return
     */
    @Override
    public Result loadList(ArticleLoadDto dto) {

        //构建分页参数
        Page<Article> page = Page.of(dto.getCurrent(), dto.getSize());

        //构建分页条件，进行查询
        Page<Article> p = lambdaQuery()
                .like(dto.getInfo() != null, Article::getTitle, dto.getInfo())
                .like(dto.getInfo() != null, Article::getTitle, dto.getInfo())
                .orderByDesc(Article::getUpdateTime)
                .page(page);

        return Result.success(p);
    }

    /**
     * 查询全部文章
     * @return
     */
    @Override
    public Result loadAllArticle() {
        List<Article> list = articleMapper.selectList(Wrappers.<Article>lambdaQuery());
        return Result.success(list);
    }

    /**
     * 查询文章评论
     * @param articleId
     * @return
     */
    @Override
    public Result loadComment(Integer articleId) {

        //构建查询条件
        Query query = new Query();
        query.addCriteria(Criteria.where("targetArticleId").is(articleId));
        query.with(Sort.by(Sort.Order.desc("createTime")));

        //查询返回集合
        List<Comment> comments = mongoTemplate.find(query, Comment.class, ParameterConstant.AP_ARTICLE_COMMENT);

        return Result.success(comments);
    }

    /**
     * 新增评论
     * @param dto
     * @return
     */
    @Override
    public Result addComment(CommentDto dto) {

        //参数构建
        Comment comment = Comment.builder()
                .content(dto.getContent())
                .targetArticleId(dto.getTargetArticleId())
                .userId(dto.getUserId())
                .createTime(LocalDateTime.now())
                .userAvatar(dto.getUserAvatar())
                .userName(dto.getUserName())
                .build();

        mongoTemplate.save(comment, ParameterConstant.AP_ARTICLE_COMMENT);

        return Result.success(MessageConstant.SUCCESS);
    }

    /**
     * 文章点赞
     * @param dto
     * @return
     */
    @Override
    public Result articleLike(ArticleUserLikeDto dto) {

        Integer articleId = dto.getArticleId();
        Integer userId = dto.getUserId();

        //判断用户是否点赞过文章
        Object isLiked = redisTemplate.opsForValue().get("ArticleLikeId-" + articleId + "-" + userId);
        if(isLiked != null) {
            return Result.error(MessageConstant.ALREADY_LIKED);
        }

        //文章没被点赞过，先加入redis键值对
        if(redisTemplate.opsForValue().get("ArticleLikeId-" + articleId) == null) {
            redisTemplate.opsForValue().set("ArticleLikeId-" + articleId, 0);
        }

        //更新redis点赞数据
        Integer likeNum = (Integer) redisTemplate.opsForValue().get("ArticleLikeId-" + articleId);
        redisTemplate.opsForValue().set("ArticleLikeId-" + articleId + "-" + userId, 1);
        redisTemplate.opsForValue().set("ArticleLikeId-" + articleId, likeNum + 1);

        return Result.success(MessageConstant.SUCCESS);
    }

    /**
     * 文章取消点赞
     * @param dto
     * @return
     */
    @Override
    public Result articleUndoLike(ArticleUserLikeDto dto) {

        Integer articleId = dto.getArticleId();
        Integer userId = dto.getUserId();

        //更新redis点赞数据
        Integer likeNum = (Integer) redisTemplate.opsForValue().get("ArticleLikeId-" + articleId);
        redisTemplate.delete("ArticleLikeId-" + articleId + "-" + userId);
        redisTemplate.opsForValue().set("ArticleLikeId-" + articleId, likeNum - 1);

        return Result.success(MessageConstant.SUCCESS);
    }

    /**
     * 获取文章点赞数量
     * @param articleId
     * @return
     */
    @Override
    public Result getArticleLikeNum(Integer articleId) {
        Integer resultNum = 0;
        Object likeNum = redisTemplate.opsForValue().get("ArticleLikeId-" + articleId);
        if(likeNum == null) {
            return Result.success(resultNum);
        }
        resultNum = (Integer) likeNum;
        return Result.success(resultNum);
    }

    /**
     * 文章搜索
     * @param dto
     * @return
     */
    @Override
    public Result searchArticle(ArticleSearchDto dto) {

        // URL and API key
        String serverUrl = "http://localhost:9200";
        String apiKey = "hz0gC8IiQiC588ePvr_DOQ";

        // Create the low-level client
        RestClient restClient = RestClient
                .builder(HttpHost.create(serverUrl))
                .setDefaultHeaders(new Header[]{
                        new BasicHeader("Authorization", "ApiKey " + apiKey)
                })
                .build();

        // Create the transport with a Jackson mapper
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());

        // And create the API client
        ElasticsearchClient esClient = new ElasticsearchClient(transport);

        // Use the client...
        log.info("文章搜索:{}", dto);
        //DSL查询
        SearchResponse<Article> response = null;
        try {
            response = esClient.search(s -> s
                            .index("ap_article")
                            .query(q -> q
                                    .match(t -> t
                                            .field("all")
                                            .query(dto.getContent())
                                    )
                            )
                            .highlight(h -> h.fields("title", f -> f.requireFieldMatch(false)
                                                                        .preTags("<text>")
                                                                        .postTags("</text>")))
                            .from(dto.getFrom())
                            .size(dto.getSize()),
                    Article.class
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        long total = 0L;
        if (response.hits().total() != null) {
            total = response.hits().total().value();
        }

        //封装article List
        List<Hit<Article>> hits = response.hits().hits();
        List<Article> list = new ArrayList<>();
        for (Hit<Article> hit : hits) {
            Article article = hit.source();
            //设置高亮结果
            Map<String, List<String>> highlight = hit.highlight();
            List<String> highlightString = highlight.get("title");
            String title = article.getTitle();
            if(highlightString != null) {
                title = highlightString.get(0);
            }
            article.setTitle(title);
            list.add(article);
            //System.out.println("Found article " + article.getTitle() + ", score " + hit.score());
        }

        // Close the client, also closing the underlying transport object and network connections.
        try {
            esClient.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        Page<Article> page = new Page<>();
        page.setRecords(list);
        page.setTotal(total);

        return Result.success(page);
    }

    /**
     * 文章搜索自动补全
     * @param content
     * @return
     */
    @Override
    public Result completionArticleSearch(String content) {

        // URL and API key
        String serverUrl = "http://localhost:9200";
        String apiKey = "hz0gC8IiQiC588ePvr_DOQ";

        // Create the low-level client
        RestClient restClient = RestClient
                .builder(HttpHost.create(serverUrl))
                .setDefaultHeaders(new Header[]{
                        new BasicHeader("Authorization", "ApiKey " + apiKey)
                })
                .build();

        // Create the transport with a Jackson mapper
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());

        // And create the API client
        ElasticsearchClient esClient = new ElasticsearchClient(transport);

        // Use the client...
        SearchResponse<Article> response = null;
        try {
            response = esClient.search(s -> s
                            .index("ap_article")
                            .suggest(g -> g.suggesters("ArticleSuggestion", f -> f
                                    .text(content)
                                    .completion(h -> h
                                            .field("suggestion")
                                            .skipDuplicates(true)
                                            .size(10)
                                    )
                            ))
                    ,
                    Article.class
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        //System.out.println(response);

        //suggestion自动补全结果解析
        List<String> completion = new ArrayList<>();
        List<CompletionSuggestOption<Article>> articleSuggestion = response.suggest().get("ArticleSuggestion").get(0).completion().options();
        for (CompletionSuggestOption<Article> option : articleSuggestion) {
            //添加补全结果
            completion.add(option.text());
        }


        // Close the client, also closing the underlying transport object and network connections.
        try {
            esClient.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return Result.success(completion);
    }
}
